package net.homeip.mmhg.game;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import net.homeip.mmhg.dao.TaskDao;
import net.homeip.mmhg.dao.TeamDao;
import net.homeip.mmhg.domain.Notification;
import net.homeip.mmhg.domain.Task;
import net.homeip.mmhg.domain.Team;
import net.homeip.mmhg.events.BaseEvent;
import net.homeip.mmhg.events.EventPublisher;
import net.homeip.mmhg.events.PositionEvent;
import net.homeip.mmhg.events.Event.EventType;
import net.homeip.mmhg.game.AttemptResultInfo.AttemptResult;
import net.homeip.mmhg.game.ObtainedTask.TaskResult;
import net.homeip.mmhg.game.ObtainedTask.TaskState;
import net.homeip.mmhg.util.DateUtil;

import org.joda.time.DateTime;
import org.joda.time.Seconds;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * Contains all information about game.
 * Responsible for transition logic.
 *
 * @author ilya gerasimov
 */
@Service
public class GameContext {
	
	private static String JOB_GROUP = "POSITION_JOBS";
	private static String NOTIF_GROUP = "NOTIF_JOBS";
	private static int DEFAULT_REFRESH_INTERVAL = 60;

	private Map<Integer, Position> positions;
	private Map<Integer, Collection<Team>> taskMap;
	private Map<Integer, Task> tasks;
	private Dispatcher dispatcher;
	private Scheduler scheduler;
	private AnswerChecker answerChecker;
	private DateTime start;
	private DateTime end;
	private int finishedTeams;
	private Map<Integer, Notification> notifies;
	private Collection<Notification> activeNotifies;
	
	private TaskDao taskDao;
	private TeamDao teamDao;
	private EventPublisher publisher;
	
	public enum GameState { NEW, STARTED, FINISHED }
		
	private GameState state;
	
	public GameContext() {
		state = GameState.NEW;
	}
	
	@Autowired
	public void setTaskDao(TaskDao taskDao) {
		this.taskDao = taskDao;
	}
	
	@Autowired
	public void setTeamDao(TeamDao teamDao) {
		this.teamDao = teamDao;
	}
	
	@Autowired
	public void setDispatcher(Dispatcher dispatcher) {
		this.dispatcher = dispatcher;
	}

	@Autowired
	public void setAnswerChecker(AnswerChecker answerChecker) {
		this.answerChecker = answerChecker;
	}
	
	@Autowired
	public void setScheduler(Scheduler scheduler) {
		this.scheduler = scheduler;
	}
	
	@Autowired
	public void setEventPublisher(EventPublisher publisher) {
		this.publisher = publisher;
	}
	
	public Collection<Position> getPositions() {
		return positions.values();
	}
	
	public Position getTeamPosition(Integer teamID) {
		return positions.get(teamID);
	}

	public GameState getState() {
		return state;
	}
	
	public boolean isStarted() {
		return (state == GameState.STARTED);
	}
	
	public void setNotifies(Collection<Notification> notifies) {
		this.notifies = new HashMap<Integer, Notification>(notifies.size());
		for (Notification notification: notifies) {
			this.notifies.put(notification.getId(), notification);
		}
	}

	public Collection<Notification> getNotifies() {
		return notifies.values();
	}
	
	public DateTime getStart() {
		return start;
	}
	
	public void setStart(DateTime start) {
		this.start = start;
	}
	
	public DateTime getEnd() {
		return end;
	}
	
	public void setEnd(DateTime end) {
		this.end = end;
	}
	
	public Collection<Task> getTasks() {
		return tasks.values();
	}
	
	public Map<Integer, Task> getTaskMap() {
		return tasks;
	}
	
	public Collection<Team> getOwns(Task task) {
		return taskMap.get(task.getId());
	}
	
	public Map<Integer, Collection<Team>> getGameMap() {
		return taskMap;
	}
	
	public boolean isFinished() {
		return (state == GameState.FINISHED);
	}
	
	public boolean isNew() {
		return (state == GameState.NEW);
	}

	public void setTeams(Collection<Team> teams) {
		positions = new HashMap<Integer, Position>(teams.size());
		for (Team team : teams) {
			positions.put(team.getId(), new Position(team));
		}
	}

	public Collection<Notification> getActiveNotifies() {
		return activeNotifies;
	}
	
	public void setTasks(Collection<Task> taskList) {
		taskMap = new HashMap<Integer, Collection<Team>>(taskList.size());
		tasks = new HashMap<Integer, Task>(taskList.size());
		for (Task task : taskList) {
			taskMap.put(task.getId(), new LinkedList<Team>());
			tasks.put(task.getId(), task);
		}
	}

	private void setTask2Team(Team team, Task task) {
		positions.get(team.getId()).setObtainedTask(new ObtainedTask(task));
		taskMap.get(task.getId()).add(team);
	}
	
	public void start() {
		start = new DateTime();
		publisher.publish(new BaseEvent(EventType.GAME_STARTED));
		// start tasks
		for (Position position : positions.values()) {
			if (!position.getTeam().isBlocked()) {
				obtainNewTask(position);
			}
		}
		
		// start notifications
		activeNotifies = new ArrayList<Notification>(notifies.size());
		for (Notification notification : notifies.values()) {
			if (notification.getTargetTime() != null) {
				scheduleNotification(notification);
			} else {
				activeNotifies.add(notification);
			}
		}

		// mark game started
		state = GameState.STARTED;
		finishedTeams = 0;
	}
	
	public void stop() {
		state = GameState.FINISHED;
		end = new DateTime();
		try {
			for (Position position : positions.values()) {
				position.getObtainedTask().finish(TaskResult.STOPPED);
				scheduler.deleteJob(position.getTeam().getId().toString(), JOB_GROUP);
			}
		} catch (Throwable throwable) {}
		
		publisher.publish(new BaseEvent(EventType.GAME_STOPPED));
	}
	
	public void changePositionState(Integer positionId) {
		Position position = positions.get(positionId);
		if (position != null) {
			ObtainedTask obtainedTask = position.getObtainedTask();
			obtainedTask.advanceState();
			if (obtainedTask.getState() != TaskState.FINISHED) {
				scheduleStdTaskJob(obtainedTask, position.getTeam());
				publisher.publish(new PositionEvent(EventType.TASK_STATE_CHANGED, position));
			} else {
				publisher.publish(new PositionEvent(EventType.TASK_FINISHED, position));
				obtainNewTask(position);
			}
		}
	}
	
	public AttemptResultInfo tryGuess(Integer teamId, String guess) {
		Position position = positions.get(teamId);
		ObtainedTask obtainedTask = position.getObtainedTask();
		String result = answerChecker.checkAnswer(obtainedTask, guess); 
		if (!guess.equals(result)) {
			if (obtainedTask.getAnswers().size() == 0) {
				// marks current task as finished
				obtainedTask.setGuessed();
				publisher.publish(new PositionEvent(EventType.TASK_FINISHED, position));
				// obtains a new task 
				obtainNewTask(position);
				return new AttemptResultInfo(AttemptResult.FINISHED);
			} else {
				if (result != null) { // some mistakes were founded
					return new AttemptResultInfo(AttemptResult.PARTIALLY_GUESSED, result);
				} else {
					return new AttemptResultInfo(AttemptResult.GUESSED);
				}
			}
		} else {
			if (obtainedTask.incrementAttempts()) {
				obtainedTask.finish(TaskResult.REJECTED);
				publisher.publish(new PositionEvent(EventType.TOO_MANY_ERRORS, position));
				obtainNewTask(position);
				return new AttemptResultInfo(AttemptResult.REJECTED);
			}
			return new AttemptResultInfo(AttemptResult.MISSED);
		}
	}
	
	private void obtainNewTask(Position position) {
		if (position.getObtainedTask() != null) {
			position.getObtainedTasks().put(position.getObtainedTask().getTask().getId(), position.getObtainedTask());
			taskMap.get(position.getObtainedTask().getTask().getId()).remove(position.getTeam());
		}
		Task task = dispatcher.obtainTask(this, position);
		if (task != null) {
			setTask2Team(position.getTeam(), task);
			scheduleStdTaskJob(position.getObtainedTask(), position.getTeam());
			publisher.publish(new PositionEvent(EventType.NEW_TASK, position));
		} else {
			position.setObtainedTask(null);
			position.setState(Position.State.FINISH_ALL_TASKS);
			finishedTeams++;
			if (finishedTeams == positions.values().size()) {
				stop();
			}
			publisher.publish(new PositionEvent(EventType.NO_MORE_TASKS, position));
		}
	}
	
	private void scheduleStdTaskJob(ObtainedTask obtainedTask, Team team) {
		try {
			scheduler.deleteJob(team.getId().toString(), JOB_GROUP);
			Trigger trigger = ChangeTaskStateJob.getStdTrigger();
			trigger.setName(team.getId().toString());
			trigger.setGroup(JOB_GROUP);
			trigger.getJobDataMap().put(ChangeTaskStateJob.POSITION_ID, team.getId());
			scheduler.scheduleJob(new JobDetail(team.getId().toString(),
										   		JOB_GROUP,
										   		ChangeTaskStateJob.class),
							   	  trigger);
		} catch (SchedulerException e) {
			e.printStackTrace();
		}
	}
	
	private void scheduleNotification(Notification notification) {
		try {
			scheduler.deleteJob(notification.getId().toString(), NOTIF_GROUP);
			DateTime dt = new DateTime(notification.getTargetTime().getTime());
			DateTime time = DateUtil.setTime(new DateTime(), dt);
			JobDetail jobDetail = new JobDetail(notification.getId().toString(), NOTIF_GROUP, ShowNotificationJob.class);
			Trigger trigger = new SimpleTrigger(notification.getId().toString(), NOTIF_GROUP, time.toDate());
			trigger.getJobDataMap().put(ShowNotificationJob.NOTIF_ID, notification);
			scheduler.scheduleJob(jobDetail, trigger);
		} catch (SchedulerException e) {
			e.printStackTrace();
		}
	}
	
	public void showNotification(Integer id) {
		Notification n = notifies.get(id);
		if (n != null) {
			activeNotifies.add(n);
		}
	}
	
	public void rejectTask(Team team) {
		Position position = positions.get(team.getId());
		if (position != null) {
			position.getObtainedTask().finish(TaskResult.REJECTED);
			obtainNewTask(position);
		}
	}
	
	public void newGame() {
		state = GameState.NEW;
		publisher.publish(new BaseEvent(EventType.GAME_RESET));
	}
	
	public int getSecUntilNextEvent(Team team) {
		Trigger trigger;
		try {
			trigger = scheduler.getTrigger(team.getId().toString(), JOB_GROUP);
			if (trigger == null) {
				return DEFAULT_REFRESH_INTERVAL;
			}
			return Seconds.secondsBetween(new DateTime(),
						new DateTime(trigger.getNextFireTime())).getSeconds();
		} catch (SchedulerException e) {
			return DEFAULT_REFRESH_INTERVAL;
		}
	}
	
	public void blockTeam(Position position) {
		position.getTeam().setBlocked(true);
		publisher.publish(new PositionEvent(EventType.TEAM_BLOCKED, position));
		if (state == GameState.STARTED) {
			position.getObtainedTask().finish(TaskResult.STOPPED);
			try {
				scheduler.deleteJob(position.getTeam().getId().toString(), JOB_GROUP);
			} catch (SchedulerException e) {
				e.printStackTrace();
			}
		}
	}

	public void unblockTeam(Position position) {
		position.getTeam().setBlocked(false);
		publisher.publish(new PositionEvent(EventType.TEAM_UNBLOCKED, position));
		if (state == GameState.STARTED) {
			obtainNewTask(position);
		}
	}
	
	@SuppressWarnings("unchecked")
	public void initialize() {
		setTasks(taskDao.getTasks());
		setTeams(teamDao.getTeams());
		//setNotifies(notificationDao.getNotifications());
		setNotifies((Collection)Collections.emptyList());
		setStart(null);
		setEnd(null);
		newGame();
	}
}
